 /**
 * 
 */
package appStarshipPushups.server;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;

import javax.swing.Timer;

import appStarshipPushups.common.CommonUse;
import appStarshipPushups.common.Laser;
import appStarshipPushups.common.Protocole;
import appStarshipPushups.common.Starship;


/**
 * Cette classe représente un fil d'exécution d'un joueur.
 * @author Adam Labranche & Kevin Arsenault
 */
public class PlayerThread extends Thread implements ActionListener
{	
	// Constantes des messages d'erreurs (ou autre).
	private static final String NOT_CONNECTED = "Vous ne pouvez exécuter cette commande, vous n'êtes pas connecté.";
	private static final String FUNCTION_NONEXISTANT = "La fonction envoyée n'existe pas.";
	private static final String EMPTY_MSG_ERR = "Le message reçu est vide de contenu.";
	private static final String CONNECT_ERR = "Il y a eu une erreur lors de la connection du client au server(PlayerThread).";
	private static final String INVALID_CONNECT_INFO = "Le mot de passe entré ou le nom d'utilisateur n'est pas valide.";
	private static final String NOT_ENOUGH_OPTIONS = "Vous n'avez pas rentré assez d'options.";
	private static final String RECIEVE_ERR= "Il y a eu un erreur lors de la réception de texte.";
	private static final String MAX_PLAYERS_ERROR = "Maximum de joueurs déjà atteint.";
	private static final String SHIP_NUMBER = "Numéro du vaisseau : ";
	private static final String LASER_NUMER = "Numéro du laser : ";
	private static final String NB_PLAYERS = "Nombre de joueurs : ";
	private static final String NB_LASERS = "Nombre de lasers : ";
	private static final String MOVE_PARAM = "Paramètre de move : ";
	private static final String MOVE_ERROR = "Il y a eu un erreur lors de la tentative de déplacement.";
	private static final String FILE_PLAYER = "Joueur(";
	private static final String FILE_INPUT_SERVER = ") serverInput : ";
	private static final String FILE_INSERVER_SINGLE = "serverInput : ";
	
	/**
	 * Cette constante représente le délai qu'il y a entre chaque vérification
	 * de collision entre le laser et des vaisseaux.
	 */
	private static final int LASER_DELAY = 150;
	
	/**
	 * Ce membre statique permet de savoir quel espace joueur est disponible.
	 */
	private static boolean[] playerSlots = {false, false, false, false, false,
			false, false, false, false, false};
	/**
	 * Ce membre static permet de savoir quel laser est activé.
	 */
	private static boolean[] laserSlots = {false, false, false, false, false,
		false, false, false, false, false};
	
	/**
	 * Ce membre représente le socket du client.
	 */
	private Socket client = null;
	/**
	 * Ce membre représente la sortie pour envoyer des messages au client.
	 */
	private PrintWriter out = null;
	/**
	 * Ce membre représente l'entrée des messages envoyés par le client.
	 */
	private BufferedReader in = null;
	/**
	 * Ce membre représente le vaisseau du joueur.
	 */
	private Starship starship = null;
	/**
	 * Ce membre représente le laser du joueur.
	 */
	private Laser laser = null;
	/**
	 * Ce membre représente la ligne de texte lue par l'entrée du PlayerThread.
	 */
	private String line = "";
	/**
	 * Ce membre représente le Timer du laser.
	 */
	private Timer timLaser = new Timer(PlayerThread.LASER_DELAY, this);
	/**
	 * Ce membre permet de savoir si le PlayerThread est connecté.
	 */	
	private boolean connected = false;
	
	/**
	 * C'est le constructeur par défaut.
	 */
	public PlayerThread(Socket clientSocket)
	{
		this.client = clientSocket;
		this.timLaser.setInitialDelay(0);
		try
		{
			this.in = new BufferedReader(new InputStreamReader(this.client.getInputStream()));
			this.out = new PrintWriter(this.client.getOutputStream(), true);
		}
		catch(IOException e)
		{
			System.err.println(PlayerThread.CONNECT_ERR);
			e.printStackTrace();
		}
	}
	
	/**
	 * Cette fonction traite le message envoyé par le client.
	 * @param msg
	 */
	public void handleMsg(String msg)
	{
		if(this.starship != null)
		{
			// Écrire le message reçu dans le fichier.
			StarshipPushupsServer.getInstance().log(PlayerThread.FILE_PLAYER + this.starship.getID() + PlayerThread.FILE_INPUT_SERVER + msg);
		}
		else
		{
			StarshipPushupsServer.getInstance().log(PlayerThread.FILE_INSERVER_SINGLE + msg);
		}
		
		// Si le message n'est pas vide.
		if(msg != "")
		{
			this.executeMessage(msg);
		}
		else
		{
			System.err.println(PlayerThread.EMPTY_MSG_ERR);
			this.sendMsg(Protocole.FUNCTION_SEND_ERROR 
					+ Protocole.SEPARATOR
					+ PlayerThread.EMPTY_MSG_ERR);
		}
	}
	
	/**
	 * Cette fonction permet de démarrer la connection avec le client.
	 */
	public void startConnection()
	{
		this.connected = true;
		this.start();
	}
	
	/**
	 * Retourne l'espace de joueur disponible le plus près de 0, retourne -1 
	 * s'il ne reste plus de place.
	 * @return L'espace de joueur disponible le plus près de 0.
	 */
	private int getAvailableSlot()
	{
		int result = -1;
		int i = 0;
		while(result == -1 && i < PlayerThread.playerSlots.length)
		{
			if(!PlayerThread.playerSlots[i])
			{
				result = i;
			}
			++i;
		}
		return result;
	}
	
	/**
	 * Sert à interpreter les commandes et à les exécuter.
	 * @param msg
	 */
	private void executeMessage(String msg)
	{
		String[] msgSplit = msg.split(Protocole.SEPARATOR_SPLIT);
		// Vérifier quel fonction doit être exécuté.
		if(msgSplit[Protocole.FUNCTION_INDEX].equals(Protocole.FUNCTION_JOIN))
		{
			// S'il y a un | qui sépare les options.
			if(msgSplit.length == Protocole.JOIN_PASSWORD + 1)
			{				
				// Si le nom d'utilisateur et le mot de passe sont correctes.
				if(msgSplit[Protocole.JOIN_USER_NAME].equals(Protocole.USER_NAME) 
						&& msgSplit[Protocole.JOIN_PASSWORD].equals(Protocole.PASSWORD))
				{
					// Indiquer que la connection désiré est accordé.
					this.connected = true;
					// On ne veut pas ajouter deux vaisseaux au même espace
					// de joueur, alors on synchronise.
					synchronized(Protocole.synchronization)
					{
						// Attribuer un vaisseau au joueur.
						int shipSlot = this.getAvailableSlot();
						System.out.println("Slot disponible : " + shipSlot);
						if(shipSlot != -1)
						{
							this.starship = CommonUse.addShip(shipSlot);
							PlayerThread.playerSlots[this.starship.getID()] = true;
							this.sendAcceptPlayer();
						}
						else
						{
							this.sendErrMsg(PlayerThread.MAX_PLAYERS_ERROR);
							System.out.println(MAX_PLAYERS_ERROR);
						}
					}
					System.out.println(PlayerThread.SHIP_NUMBER + Protocole.getShips().indexOf(this.starship));
				}
				else
				{
					System.err.println(PlayerThread.INVALID_CONNECT_INFO);
					this.sendErrMsg(PlayerThread.INVALID_CONNECT_INFO);
				}
				
				System.out.println(PlayerThread.NB_PLAYERS + Protocole.getShips().size());
			}
			else // Il y a une erreur.
			{
				System.err.println(PlayerThread.NOT_ENOUGH_OPTIONS);
				this.sendErrMsg(PlayerThread.NOT_ENOUGH_OPTIONS);

			}
		}
		else if(msgSplit[Protocole.FUNCTION_INDEX].equals(Protocole.FUNCTION_MOVE))
		{
			// Si la connection est établie.
			if(this.connected)
			{
				System.out.println(PlayerThread.MOVE_PARAM + msgSplit[1]);
				
				boolean worked = false;
				if(Protocole.getShips().indexOf(this.starship) <= Protocole.getShips().size())
				{
					worked = CommonUse.moveShip(Protocole.getShips().indexOf(this.starship), msgSplit[1]);
				}
				
				// Si ça a fonctionné.
				if(worked)
				{
					StarshipPushupsServer.getInstance().broadcast(Protocole.FUNCTION_SET_PLAYER
							+ Protocole.SEPARATOR + this.starship.getID()
							+ Protocole.SEPARATOR 
							+ Protocole.getShips().get(Protocole.getShips().indexOf(this.starship)).getPosition().x
							+ Protocole.SEPARATOR
							+ Protocole.getShips().get(Protocole.getShips().indexOf(this.starship)).getPosition().y
							+ Protocole.SEPARATOR
							+ Protocole.getShips().get(Protocole.getShips().indexOf(this.starship)).getDirection());
				}
				else
				{
					System.err.println(PlayerThread.MOVE_ERROR);
				}
			}
			else
			{
				System.err.println(PlayerThread.NOT_CONNECTED);
				this.sendErrMsg(PlayerThread.NOT_CONNECTED);
			}
		}
		else if(msgSplit[Protocole.FUNCTION_INDEX].equals(Protocole.FUNCTION_SHOOT_ON))
		{
			// Si la connection est établie
			if(this.connected)
			{
				// Créer le laser et prendre son index.
				synchronized(Protocole.synchronization)
				{
					if(this.laser == null && this.starship != null)
					{
						this.laser = CommonUse.addLaser(Protocole.getShips().indexOf(this.starship),
								this.starship.getID());
					}
				}
				if(this.laser != null && this.starship != null && !this.timLaser.isRunning())
				{
					this.timLaser.start();
				}
				System.out.println(PlayerThread.LASER_NUMER + Protocole.getLasers().indexOf(this.laser));
				
				// Les informations concernant les coordonnées du laser.
				int x1 = Protocole.getLasers().get(Protocole.getLasers().indexOf(this.laser)).getLaserEmplacement().getPosition().x;
				int y1 = Protocole.getLasers().get(Protocole.getLasers().indexOf(this.laser)).getLaserEmplacement().getPosition().y;
				int x2 = Protocole.getLasers().get(Protocole.getLasers().indexOf(this.laser)).getLaserEmplacement().getSizePosition().x;
				int y2 = Protocole.getLasers().get(Protocole.getLasers().indexOf(this.laser)).getLaserEmplacement().getSizePosition().y;
				
				// Indiquer au joueurs qu'un laser vient de se faire tirer.
				StarshipPushupsServer.getInstance().broadcast(Protocole.FUNCTION_ADD_LASER
						+ Protocole.SEPARATOR + this.laser.getID()
						+ Protocole.SEPARATOR + x1
						+ Protocole.SEPARATOR + y1
						+ Protocole.SEPARATOR + x2
						+ Protocole.SEPARATOR + y2);
			}
			else
			{
				System.err.println(PlayerThread.NOT_CONNECTED);
				this.sendErrMsg(PlayerThread.NOT_CONNECTED);
			}
		}
		else if(msgSplit[Protocole.FUNCTION_INDEX].equals(Protocole.FUNCTION_SHOOT_OFF))
		{
			// Si la connection est établie
			if(this.connected)
			{
				// Enlever le laser de la liste de laser.
				System.out.println(PlayerThread.NB_LASERS + Protocole.getLasers().size());
				synchronized(Protocole.synchronization)
				{
					this.stopLaser();
				}
			}
			else
			{
				System.err.println(PlayerThread.NOT_CONNECTED);
				this.sendErrMsg(PlayerThread.NOT_CONNECTED);
			}
		}
		else if(msgSplit[Protocole.FUNCTION_INDEX].equals(Protocole.FUNCTION_QUIT))
		{
			// Si la connection est établie
			if(this.connected)
			{
				this.quitGame();
			}
			else
			{
				System.err.println(PlayerThread.NOT_CONNECTED);
				this.sendErrMsg(PlayerThread.NOT_CONNECTED);
			}
		}
		else // Il y a un erreur.
		{
			System.err.println(PlayerThread.FUNCTION_NONEXISTANT);
			this.sendErrMsg(PlayerThread.FUNCTION_NONEXISTANT);
		}
	}
	
	/**
	 * Cette fonction s'occupe de faire quitter le joueur de la partie.
	 */
	public void quitGame()
	{
		PlayerThread.playerSlots[this.starship.getID()] = false;
		this.connected = false;
		StarshipPushupsServer.getInstance().removePlayerThread(this);
		StarshipPushupsServer.getInstance().broadcastErasePlayer(this.starship.getID());
		Protocole.getShips().remove(this.starship);
		this.stopLaser();
		this.sendLeaveOk();
		
	}
	
	/**
	 * Cette fonciton envoye un message au client.
	 * @param msg Le message à envoyer.
	 */
	public synchronized void sendMsg(String msg)
	{
		// Écrire le message reçu dans le fichier.
		StarshipPushupsServer.getInstance().log(PlayerThread.FILE_PLAYER 
				+ this.starship.getID() + PlayerThread.FILE_INPUT_SERVER + msg);
		
		if(this.client.isBound())
		{
			this.out.println(msg);
		}
		else
		{
			this.connected = false;
			Protocole.getShips().remove(Protocole.getShips().indexOf(this.starship));
		}
	}
	
	/**
	 * Cette fonction permet d'envoyer un message d'erreur au client.
	 * @param errMsg Le message d'erreur à envoyer.
	 */
	public void sendErrMsg(String errMsg)
	{
		this.sendMsg(Protocole.FUNCTION_SEND_ERROR
				+ Protocole.SEPARATOR
				+ errMsg);
	}
	
	/**
	 * Cette fonction permet d'envoyer un message avertissant le joueur qu'il
	 * doit se déconnecter.
	 */
	public void sendLeaveOk()
	{
		this.sendMsg(Protocole.FUNCTION_LEAVEOK);
	}
	
	/**
	 * Cette fonction permet d'envoyer un message avertissant le joueur
	 * qu'il a été accepté.
	 */
	public void sendAcceptPlayer()
	{
		this.sendMsg(Protocole.FUNCTION_ACCEPT_PLAYER
				+ Protocole.SEPARATOR
				+ this.starship.getID());
		System.out.println(Integer.toString(Protocole.getShips().size()));
		// On informe tout le monde de la venu du nouveau joueur.
		StarshipPushupsServer.getInstance().broadcast(Protocole.FUNCTION_SET_PLAYER
					+ Protocole.SEPARATOR + this.starship.getID()
					+ Protocole.SEPARATOR 
					+ this.starship.getPosition().x
					+ Protocole.SEPARATOR
					+ this.starship.getPosition().y
					+ Protocole.SEPARATOR
					+ this.starship.getDirection());
		
		// On envoie l'information de tous les joueurs au joueur qui vient de
		// joindre la partie.
		for(int i = 0; i < Protocole.getShips().size(); i++)
		{
			System.out.println("Sending ship ID : " + Protocole.getShips().get(i).getID());
			this.out.println(Protocole.FUNCTION_SET_PLAYER
					+ Protocole.SEPARATOR + Protocole.getShips().get(i).getID()
					+ Protocole.SEPARATOR 
					+ Protocole.getShips().get(i).getPosition().x
					+ Protocole.SEPARATOR
					+ Protocole.getShips().get(i).getPosition().y
					+ Protocole.SEPARATOR
					+ Protocole.getShips().get(i).getDirection());
		}
		
		// On envoie l'information de tous les lasers au joueur qui vient de
		// joindre la partie.
		for(int i = 0; i < Protocole.getLasers().size(); i++)
		{
			this.out.println(Protocole.FUNCTION_ADD_LASER
					+ Protocole.SEPARATOR
					+ i
					+ Protocole.getLasers().get(i).getLaserEmplacement().getPosition().x
					+ Protocole.SEPARATOR
					+ Protocole.getLasers().get(i).getLaserEmplacement().getPosition().y
					+ Protocole.SEPARATOR
					+ Protocole.getLasers().get(i).getLaserEmplacement().getSizePosition().x
					+ Protocole.SEPARATOR
					+ Protocole.getLasers().get(i).getLaserEmplacement().getSizePosition().y);
		}
		
	}
	
	/**
	 * Cette fonction est exécuté lors du démarrage du PlayerThread.
	 */
	public void run()
	{
		while(this.connected)
		{
			try
			{
				this.line = this.in.readLine();
				if(this.line != null)
				{
					this.handleMsg(this.line);
				}
			}
			catch(IOException e)
			{
				System.err.println(PlayerThread.RECIEVE_ERR);
				System.err.println(e.getMessage());
				this.connected = false;
				Protocole.getShips().remove(Protocole.getShips().indexOf(this.starship));
			}
		}
	}
	
	/**
	 * Cette fonction termine le fil d'exécution du PlayerThread.
	 */
	public void end()
	{
		try
		{
			this.in.close();
			this.client.close();
			this.out.close();
		}
		catch(IOException e)
		{
			System.err.println(e.getMessage());
		}
	}
	
	/**
	 * Cette fonction arrête le laser du joueur (s'il y en a un).
	 */
	private void stopLaser()
	{
		if(this.laser != null)
		{
			PlayerThread.laserSlots[this.laser.getID()] = false;
			this.timLaser.stop();
			
			// Indiquer aux joueur que le laser n'existe plus.
			StarshipPushupsServer.getInstance().broadcast(Protocole.FUNCTION_REMOVE_LASER 
					+ Protocole.SEPARATOR + Protocole.getLasers().get(Protocole.getLasers().indexOf(this.laser)).getID());
			Protocole.getLasers().remove(this.laser);
			this.laser = null;
		}
	}
	
	/**
	 * Cette fonction envoye au client l'information d'un vaisseau à supprimer.
	 * @param shipID Le numéro d'identification du vaisseau à supprimer.
	 */
	public void sendErasePlayer(int shipID)
	{
		this.sendMsg(Protocole.FUNCTION_ERASE_PLAYER
				+ Protocole.SEPARATOR
				+ shipID);
		PlayerThread.laserSlots[shipID] = false;
	}
	
    /**
     * Appelée lorsqu'une action se produit.
     */
    public void actionPerformed(ActionEvent e)
    {
    	System.out.println(e.getSource().toString());
    	// Si c'est un Timer qui a appellé la fonction.
    	if(e.getSource() instanceof Timer)
    	{
    		// On vérifie si le laser touche à des vaisseux.
    		// Pour chaque vaisseau.
			boolean needsLaserStop = false;
			for(int j = 0; j < Protocole.getShips().size(); j++)
			{
				// S'il y a collision entre le laser et le vaisseau.
				if(j != Protocole.getShips().indexOf(this.starship))
				{
					synchronized(Protocole.synchronization)
					{
						if(CommonUse.laserCollides(this.laser, Protocole.getShips().get(j)))
						{
							System.out.println(Integer.toString(Protocole.getShips().get(j).getPosition().x)
									+ " "
									+ Protocole.getShips().get(j).getHP());
							// On modifie les points de vie du vaisseau.
							Protocole.getShips().get(j).modHP(Protocole.LASER_DMG);
							// Si le vaisseau est mort, on dit au joueurs d'effacer
							// Le joueur en question.
							if(Protocole.getShips().get(j).getHP() <= 0)
							{
								System.out.println("Player ID tué et envoyé : " + Protocole.getShips().get(j).getID());
								StarshipPushupsServer.getInstance().deletePlayer(Protocole.getShips().get(j));
								--j;
								needsLaserStop = true;
							}
						}
					}
				}
			}
			synchronized(Protocole.synchronization)
			{
				if(needsLaserStop)
				{
					this.stopLaser();
				}
			}
			
    	}
    }
    
    /**
     * Cette fonction permet d'obtenir le vaisseau du joueur.
     * @return Le vaisseau du joueur.
     */
    public Starship getStarship()
    {
    	return this.starship;
    }
}
